Jelajahi Clustered Forward Rendering di WebGL, sebuah teknik andal untuk me-render ratusan cahaya dinamis secara real-time. Pelajari konsep inti dan strategi optimalisasinya.
Membuka Kunci Performa: Penyelaman Mendalam ke dalam Clustered Forward Rendering WebGL dan Optimalisasi Pengindeksan Cahaya
Di dunia grafika 3D real-time di web, me-render banyak cahaya dinamis selalu menjadi tantangan performa yang signifikan. Sebagai pengembang, kita berusaha untuk menciptakan adegan yang lebih kaya dan imersif, tetapi setiap sumber cahaya tambahan dapat secara eksponensial meningkatkan biaya komputasi, mendorong WebGL hingga batasnya. Teknik rendering tradisional sering kali memaksa pilihan yang sulit: mengorbankan ketajaman visual demi performa, atau menerima frame rate yang lebih rendah. Tapi bagaimana jika ada cara untuk mendapatkan yang terbaik dari kedua dunia?
Masuklah Clustered Forward Rendering, juga dikenal sebagai Forward+. Teknik andal ini menawarkan solusi canggih, menggabungkan kesederhanaan dan fleksibilitas material dari forward rendering tradisional dengan efisiensi pencahayaan dari deferred shading. Ini memungkinkan kita untuk me-render adegan dengan ratusan, atau bahkan ribuan, cahaya dinamis sambil mempertahankan frame rate yang interaktif.
Artikel ini memberikan eksplorasi komprehensif tentang Clustered Forward Rendering dalam konteks WebGL. Kita akan membedah konsep-konsep inti, dari membagi frustum pandangan hingga memilah cahaya, dan berfokus secara intens pada optimalisasi yang paling penting: saluran data pengindeksan cahaya. Inilah mekanisme yang secara efisien mengkomunikasikan cahaya mana yang memengaruhi bagian layar mana dari CPU ke fragment shader GPU.
Lanskap Rendering: Forward vs. Deferred
Untuk menghargai mengapa clustered rendering begitu efektif, kita harus terlebih dahulu memahami keterbatasan metode-metode yang mendahuluinya.
Forward Rendering Tradisional
Ini adalah pendekatan rendering yang paling langsung. Untuk setiap objek, vertex shader memproses vertex-nya, dan fragment shader menghitung warna akhir untuk setiap piksel. Ketika menyangkut pencahayaan, fragment shader biasanya melakukan loop melalui setiap cahaya di adegan dan mengakumulasi kontribusinya. Masalah utamanya adalah skalabilitasnya yang buruk. Biaya komputasi kira-kira sebanding dengan (Jumlah Fragmen) x (Jumlah Cahaya). Dengan hanya beberapa lusin cahaya, performa bisa anjlok, karena setiap piksel secara berlebihan memeriksa setiap cahaya, bahkan yang berjarak bermil-mil atau di balik dinding.
Deferred Shading
Deferred Shading dikembangkan untuk menyelesaikan masalah ini. Ini memisahkan geometri dari pencahayaan dalam proses dua-langkah:
- Langkah Geometri (Geometry Pass): Geometri adegan di-render ke beberapa tekstur layar penuh yang secara kolektif dikenal sebagai G-buffer. Tekstur-tekstur ini menyimpan data seperti posisi, normal, dan properti material (misalnya, albedo, roughness) untuk setiap piksel.
- Langkah Pencahayaan (Lighting Pass): Sebuah quad layar penuh digambar. Untuk setiap piksel, fragment shader mengambil sampel dari G-buffer untuk merekonstruksi properti permukaan dan kemudian menghitung pencahayaan. Keuntungan utamanya adalah pencahayaan hanya dihitung sekali per piksel, dan mudah untuk menentukan cahaya mana yang memengaruhi piksel tersebut berdasarkan posisi dunianya.
Meskipun sangat efisien untuk adegan dengan banyak cahaya, deferred shading memiliki kelemahannya sendiri, terutama untuk WebGL. Ia memiliki kebutuhan bandwidth memori yang tinggi karena G-buffer, kesulitan dengan transparansi (yang memerlukan langkah forward rendering terpisah), dan mempersulit penggunaan teknik anti-aliasing seperti MSAA.
Kasus untuk Jalan Tengah: Forward+
Clustered Forward Rendering memberikan kompromi yang elegan. Ia mempertahankan sifat satu-langkah dan fleksibilitas material dari forward rendering tetapi menggabungkan langkah pra-pemrosesan untuk secara dramatis mengurangi jumlah perhitungan cahaya per fragmen. Ia menghindari G-buffer yang berat, membuatnya lebih ramah memori dan kompatibel dengan transparansi dan MSAA secara langsung.
Konsep Inti Clustered Forward Rendering
Gagasan utama dari clustered rendering adalah menjadi lebih pintar tentang cahaya mana yang kita periksa. Alih-alih setiap piksel memeriksa setiap cahaya, kita dapat menentukan sebelumnya cahaya mana yang cukup dekat untuk mungkin memengaruhi suatu wilayah layar dan membuat piksel di wilayah itu hanya memeriksa cahaya-cahaya tersebut.
Ini dicapai dengan membagi frustum pandangan kamera menjadi sebuah grid 3D dari volume-volume yang lebih kecil yang disebut cluster (atau tile).
Proses keseluruhan dapat dipecah menjadi empat tahap utama:
- 1. Pembuatan Grid Cluster: Mendefinisikan dan membangun grid 3D yang mempartisi frustum pandangan. Grid ini tetap dalam ruang pandang dan bergerak bersama kamera.
- 2. Penugasan Cahaya (Pemilahan): Untuk setiap cluster di grid, tentukan daftar semua cahaya yang volume pengaruhnya berpotongan dengannya. Ini adalah langkah pemilahan yang krusial.
- 3. Pengindeksan Cahaya: Inilah fokus kita. Kita mengemas hasil dari langkah penugasan cahaya ke dalam struktur data yang ringkas yang dapat secara efisien dikirim ke GPU dan dibaca oleh fragment shader.
- 4. Shading: Selama langkah rendering utama, fragment shader pertama-tama menentukan cluster mana ia berada. Kemudian ia menggunakan data pengindeksan cahaya untuk mengambil daftar cahaya yang relevan untuk cluster tersebut dan melakukan perhitungan pencahayaan *hanya* untuk subset kecil cahaya tersebut.
Penyelaman Mendalam: Membangun Grid Cluster
Fondasi dari teknik ini adalah grid yang terstruktur dengan baik. Pilihan yang dibuat di sini secara langsung memengaruhi efisiensi pemilahan dan performa.
Menentukan Dimensi Grid
Grid didefinisikan oleh resolusinya di sepanjang sumbu X, Y, dan Z (misalnya, 16x9x24 cluster). Pilihan dimensi adalah sebuah trade-off:
- Resolusi Lebih Tinggi (Lebih Banyak Cluster): Menghasilkan pemilahan cahaya yang lebih ketat dan akurat. Lebih sedikit cahaya yang akan ditugaskan per cluster, yang berarti lebih sedikit pekerjaan untuk fragment shader. Namun, ini meningkatkan overhead dari langkah penugasan cahaya di CPU dan jejak memori dari struktur data cluster.
- Resolusi Lebih Rendah (Lebih Sedikit Cluster): Mengurangi overhead di sisi CPU dan memori tetapi menghasilkan pemilahan yang lebih kasar. Setiap cluster lebih besar, sehingga akan berpotongan dengan lebih banyak cahaya, yang menyebabkan lebih banyak pekerjaan di fragment shader.
Praktik umum adalah mengikat dimensi X dan Y dengan rasio aspek layar, misalnya, membagi layar menjadi 16x9 tile. Dimensi Z sering kali menjadi yang paling penting untuk disesuaikan.
Pembagian Z Logaritmik: Optimalisasi Kritis
Jika kita membagi kedalaman frustum (sumbu Z) menjadi irisan-irisan linier, kita akan menghadapi masalah yang berkaitan dengan proyeksi perspektif. Sejumlah besar detail geometris terkonsentrasi di dekat kamera, sementara objek yang jauh hanya menempati sedikit piksel. Pembagian Z linier akan menciptakan cluster yang besar dan tidak presisi di dekat kamera (di mana presisi paling dibutuhkan) dan cluster yang kecil dan boros di kejauhan.
Solusinya adalah pembagian Z logaritmik (atau eksponensial). Ini menciptakan cluster yang lebih kecil dan lebih presisi di dekat kamera dan secara progresif cluster yang lebih besar lebih jauh, menyelaraskan distribusi cluster dengan cara kerja proyeksi perspektif. Ini memastikan jumlah fragmen yang lebih seragam per cluster dan menghasilkan pemilahan yang jauh lebih efektif.
Sebuah formula untuk menghitung kedalaman `z` untuk irisan ke-i dari total `N` irisan, dengan diketahui bidang dekat `n` dan bidang jauh `f`, dapat dinyatakan sebagai:
z_i = n * (f/n)^(i/N)Formula ini memastikan bahwa rasio kedalaman irisan yang berurutan adalah konstan, menciptakan distribusi eksponensial yang diinginkan.
Inti Permasalahan: Pemilahan dan Pengindeksan Cahaya
Di sinilah keajaibannya terjadi. Setelah grid kita didefinisikan, kita perlu mencari tahu cahaya mana yang memengaruhi cluster mana dan kemudian mengemas informasi ini untuk GPU. Di WebGL, logika pemilahan cahaya ini biasanya dieksekusi di CPU menggunakan JavaScript untuk setiap frame di mana cahaya atau kamera bergerak.
Tes Perpotongan Cahaya-Cluster
Prosesnya secara konseptual sederhana: lakukan loop melalui setiap cahaya dan uji perpotongannya terhadap volume pembatas setiap cluster. Volume pembatas untuk sebuah cluster adalah frustum itu sendiri. Tes umum meliputi:
- Point Lights: Diperlakukan sebagai bola. Tesnya adalah perpotongan bola-frustum.
- Spot Lights: Diperlakukan sebagai kerucut. Tesnya adalah perpotongan kerucut-frustum, yang lebih kompleks.
- Directional Lights: Ini sering dianggap memengaruhi segalanya, jadi biasanya ditangani secara terpisah dan tidak termasuk dalam proses pemilahan.
Menjalankan tes-tes ini secara efisien adalah kuncinya. Setelah langkah ini, kita memiliki pemetaan, mungkin dalam array JavaScript dari array, seperti: clusterLights[clusterId] = [lightId1, lightId2, ...].
Tantangan Struktur Data: Dari CPU ke GPU
Bagaimana kita mengirimkan daftar cahaya per-cluster ini ke fragment shader? Kita tidak bisa begitu saja memberikan array dengan panjang variabel. Shader membutuhkan cara yang dapat diprediksi untuk mencari data ini. Di sinilah pendekatan Daftar Cahaya Global dan Daftar Indeks Cahaya masuk. Ini adalah metode elegan untuk meratakan struktur data kompleks kita menjadi tekstur yang ramah GPU.
Kita membuat dua struktur data utama:
- Tekstur Grid Informasi Cluster: Ini adalah tekstur 3D (atau tekstur 2D yang meniru 3D) di mana setiap texel sesuai dengan satu cluster di grid kita. Setiap texel menyimpan dua informasi penting:
- Sebuah offset: Ini adalah indeks awal di struktur data kedua kita (Daftar Cahaya Global) di mana cahaya untuk cluster ini dimulai.
- Sebuah count: Ini adalah jumlah cahaya yang memengaruhi cluster ini.
- Tekstur Daftar Cahaya Global: Ini adalah daftar 1D sederhana (disimpan dalam tekstur 2D) yang berisi urutan gabungan dari semua indeks cahaya untuk semua cluster.
Memvisualisasikan Aliran Data
Mari kita bayangkan skenario sederhana:
- Cluster 0 dipengaruhi oleh cahaya dengan indeks [5, 12].
- Cluster 1 dipengaruhi oleh cahaya dengan indeks [8, 5, 20].
- Cluster 2 dipengaruhi oleh cahaya dengan indeks [7].
Daftar Cahaya Global: [5, 12, 8, 5, 20, 7, ...]
Grid Informasi Cluster:
- Texel untuk Cluster 0:
{ offset: 0, count: 2 } - Texel untuk Cluster 1:
{ offset: 2, count: 3 } - Texel untuk Cluster 2:
{ offset: 5, count: 1 }
Implementasi di WebGL & GLSL
Sekarang mari kita hubungkan konsep-konsep tersebut dengan kode. Implementasinya melibatkan bagian JavaScript untuk pemilahan dan persiapan data, dan bagian GLSL untuk shading.
Transfer Data ke GPU (JavaScript)
Setelah melakukan pemilahan cahaya di CPU, Anda akan memiliki data grid cluster Anda (pasangan offset/count) dan daftar cahaya global Anda. Ini perlu diunggah ke GPU setiap frame.
- Kemas dan Unggah Data Cluster: Buat sebuah `Float32Array` atau `Uint32Array` untuk data cluster Anda. Anda dapat mengemas offset dan count untuk setiap cluster ke dalam channel RG dari sebuah tekstur. Gunakan `gl.texImage2D` untuk membuat atau `gl.texSubImage2D` untuk memperbarui tekstur dengan data ini. Ini akan menjadi tekstur Grid Informasi Cluster Anda.
- Unggah Daftar Cahaya Global: Demikian pula, ratakan indeks cahaya Anda menjadi `Uint32Array` dan unggah ke tekstur lain.
- Unggah Properti Cahaya: Semua data cahaya (posisi, warna, intensitas, radius, dll.) harus disimpan dalam tekstur besar atau Uniform Buffer Object (UBO) untuk pencarian berindeks yang cepat dari shader.
Logika Fragment Shader (GLSL)
Fragment shader adalah tempat di mana peningkatan performa direalisasikan. Berikut adalah logika langkah-demi-langkah:
Langkah 1: Tentukan Indeks Cluster Fragmen
Pertama, kita perlu tahu di cluster mana fragmen saat ini berada. Ini membutuhkan posisinya di ruang pandang.
// Uniform yang menyediakan informasi grid
uniform vec3 u_gridDimensions; // cth., vec3(16.0, 9.0, 24.0)
uniform vec2 u_screenDimensions;
uniform float u_nearPlane;
uniform float u_farPlane;
// Fungsi untuk mendapatkan indeks irisan Z dari kedalaman ruang pandang
float getClusterZIndex(float viewZ) {
// viewZ negatif, buat menjadi positif
viewZ = -viewZ;
// Invers dari formula logaritmik yang kita gunakan di CPU
float slice = floor(log(viewZ / u_nearPlane) / log(u_farPlane / u_nearPlane) * u_gridDimensions.z);
return slice;
}
// Logika utama untuk mendapatkan indeks cluster 3D
vec3 getClusterIndex() {
// Dapatkan indeks X dan Y dari koordinat layar
float clusterX = floor(gl_FragCoord.x / u_screenDimensions.x * u_gridDimensions.x);
float clusterY = floor(gl_FragCoord.y / u_screenDimensions.y * u_gridDimensions.y);
// Dapatkan indeks Z dari posisi Z ruang pandang fragmen (v_viewPos.z)
float clusterZ = getClusterZIndex(v_viewPos.z);
return vec3(clusterX, clusterY, clusterZ);
}
Langkah 2: Ambil Data Cluster
Menggunakan indeks cluster, kita mengambil sampel tekstur Grid Informasi Cluster kita untuk mendapatkan offset dan count untuk daftar cahaya fragmen ini.
uniform sampler2D u_clusterTexture; // Tekstur yang menyimpan offset dan count
// ... di dalam main() ...
vec3 clusterIndex = getClusterIndex();
// Ratakan indeks 3D ke koordinat tekstur 2D jika perlu
vec2 clusterTexCoord = ...;
vec2 lightData = texture2D(u_clusterTexture, clusterTexCoord).rg;
int offset = int(lightData.x);
int count = int(lightData.y);
Langkah 3: Loop dan Akumulasi Pencahayaan
Ini adalah langkah terakhir. Kita menjalankan loop pendek yang terbatas. Untuk setiap iterasi, kita mengambil indeks cahaya dari Daftar Cahaya Global, lalu menggunakan indeks itu untuk mendapatkan properti lengkap cahaya dan menghitung kontribusinya.
uniform sampler2D u_globalLightIndexTexture;
uniform sampler2D u_lightPropertiesTexture; // UBO akan lebih baik
vec3 finalColor = vec3(0.0);
for (int i = 0; i < count; i++) {
// 1. Dapatkan indeks cahaya yang akan diproses
int lightIndex = int(texture2D(u_globalLightIndexTexture, vec2(float(offset + i), 0.0)).r);
// 2. Ambil properti cahaya menggunakan indeks ini
Light currentLight = getLightProperties(lightIndex, u_lightPropertiesTexture);
// 3. Hitung kontribusi cahaya ini
finalColor += calculateLight(currentLight, surfaceProperties, viewDir);
}
Dan selesai! Alih-alih loop yang berjalan ratusan kali, kita sekarang memiliki loop yang mungkin berjalan 5, 10, atau 30 kali, tergantung pada kepadatan cahaya di bagian spesifik dari adegan itu, yang mengarah pada peningkatan performa yang monumental.
Optimalisasi Lanjutan dan Pertimbangan Masa Depan
- CPU vs. Compute: Hambatan utama dari teknik ini di WebGL adalah bahwa pemilahan cahaya terjadi di CPU dalam JavaScript. Ini bersifat single-threaded dan memerlukan sinkronisasi data dengan GPU setiap frame. Kedatangan WebGPU adalah pengubah permainan. Compute shader-nya akan memungkinkan seluruh proses pembuatan cluster dan pemilahan cahaya dialihkan ke GPU, menjadikannya paralel dan berkali-kali lipat lebih cepat.
- Manajemen Memori: Perhatikan memori yang digunakan oleh struktur data Anda. Untuk grid 16x9x24 (3.456 cluster) dan maksimal, katakanlah, 64 cahaya per cluster, daftar cahaya global berpotensi menampung 221.184 indeks. Menyesuaikan grid Anda dan menetapkan maksimum yang realistis untuk cahaya per cluster sangat penting.
- Menyesuaikan Grid: Tidak ada satu pun angka ajaib untuk dimensi grid. Konfigurasi optimal sangat bergantung pada konten adegan Anda, perilaku kamera, dan perangkat keras target. Melakukan profiling dan bereksperimen dengan ukuran grid yang berbeda sangat penting untuk mencapai performa puncak.
Kesimpulan
Clustered Forward Rendering lebih dari sekadar keingintahuan akademis; ini adalah solusi praktis dan andal untuk masalah signifikan dalam grafika web real-time. Dengan membagi ruang pandang secara cerdas dan melakukan langkah pemilahan dan pengindeksan cahaya yang sangat dioptimalkan, ia memutus hubungan langsung antara jumlah cahaya dan biaya fragment shader.
Meskipun memperkenalkan lebih banyak kompleksitas di sisi CPU dibandingkan dengan forward rendering tradisional, hasil performanya sangat besar, memungkinkan pengalaman yang lebih kaya, lebih dinamis, dan menarik secara visual langsung di browser. Inti keberhasilannya terletak pada saluran pengindeksan cahaya yang efisien—jembatan yang mengubah masalah spasial yang kompleks menjadi loop sederhana yang terbatas di GPU.
Seiring platform web berevolusi dengan teknologi seperti WebGPU, teknik-teknik seperti Clustered Forward Rendering akan menjadi semakin mudah diakses dan berkinerja tinggi, semakin mengaburkan batas antara aplikasi 3D native dan berbasis web.